package karma.pool.pool.proxy;

import karma.pool.pool.PoolEntry;
import karma.pool.util.LastElementList;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;

import java.sql.*;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Executor;

import static karma.pool.util.clock.ClockFactory.currentTime;


@Slf4j
public abstract class ProxyConnection implements Connection {
   public static final int dirty_status_readonly = 0b000001;
   public static final int dirty_status_autocommit = 0b000010;
   public static final int dirty_status_transaction_isolation_level = 0b000100;
   public static final int dirty_status_catalog = 0b001000;
   public static final int dirty_status_nettimeout = 0b010000;
   public static final int dirty_status_schema = 0b100000;


   private static final Set<String> errorStateSet;
   private static final Set<Integer> errorCodeSet;

   // static initializer
   static {


      errorStateSet = new HashSet<String>();
      errorStateSet.add("0A000"); // FEATURE UNSUPPORTED
      errorStateSet.add("57P01"); // ADMIN SHUTDOWN
      errorStateSet.add("57P02"); // CRASH SHUTDOWN
      errorStateSet.add("57P03"); // CANNOT CONNECT NOW
      errorStateSet.add("01002"); // SQL92 disconnect error
      errorStateSet.add("JZ0C0"); // Sybase disconnect error
      errorStateSet.add("JZ0C1"); // Sybase disconnect error

      errorCodeSet = new HashSet<>();
      errorCodeSet.add(500150);
      errorCodeSet.add(2399);
   }

   @Setter
   @Getter
   private final PoolEntry poolEntry;
   @Setter
   @Getter
   private final ProxyLeakReportRunnable proxyLeakReportRunnable;
   @Setter
   @Getter
   private final List<Statement> statementList;
   @Setter
   @Getter
   @SuppressWarnings("WeakerAccess")
   protected Connection connection;//真实的
   @Setter
   @Getter
   private int dirtyStatus;
   @Setter
   @Getter
   private long lastAccess;
   @Setter
   @Getter
   private boolean commitStateIsDirty;
   @Setter
   @Getter
   private boolean isReadOnly;
   @Setter
   @Getter
   private boolean isAutoCommit;
   @Setter
   @Getter
   private int networkTimeout;
   @Setter
   @Getter
   private int transactionIsolation;
   @Setter
   @Getter
   private String catalog;
   @Setter
   @Getter
   private String schema;

   protected ProxyConnection(final PoolEntry poolEntry, final Connection connection, final LastElementList<Statement> statementList, final ProxyLeakReportRunnable proxyLeakReportRunnable, final long now, final boolean isReadOnly, final boolean isAutoCommit) {
      this.poolEntry = poolEntry;
      this.connection = connection;
      this.statementList = statementList;
      this.proxyLeakReportRunnable = proxyLeakReportRunnable;
      this.lastAccess = now;
      this.isReadOnly = isReadOnly;
      this.isAutoCommit = isAutoCommit;
   }


   final SQLException checkException(SQLException e) {
      SQLException nse = e;
      for (int depth = 0; connection != ClosedConnection.CLOSED_CONNECTION && nse != null && depth < 10; depth++) {
         final String sqlState = nse.getSQLState();
         if (sqlState != null && sqlState.startsWith("08")
            || nse instanceof SQLTimeoutException
            || errorStateSet.contains(sqlState)
            || errorCodeSet.contains(nse.getErrorCode())) {

            // broken proxyConnection
            log.warn("{} - Connection {} marked as broken because of SQLSTATE({}), ErrorCode({})",
               poolEntry.getPoolName(), connection, sqlState, nse.getErrorCode(), nse);
            proxyLeakReportRunnable.cancel();
            poolEntry.evict("(proxyConnection is broken)");
            connection = ClosedConnection.CLOSED_CONNECTION;
         } else {
            nse = nse.getNextException();
         }
      }

      return e;
   }


   final void markCommitStateAsDirty() {
      if (isAutoCommit) {
         lastAccess = currentTime();
      } else {
         commitStateIsDirty = true;
      }
   }

   public void cancelLeakTask() {
      proxyLeakReportRunnable.cancel();
   }

   //
   private synchronized <Statement extends java.sql.Statement> Statement addStatement(final Statement statement) {
      statementList.add(statement);

      return statement;
   }

   final synchronized void removeStatement(final Statement statement) {
      statementList.remove(statement);
   }

   private synchronized void closeStatements() {
      final int size = statementList.size();
      if (size > 0) {
         for (int i = 0; i < size && connection != ClosedConnection.CLOSED_CONNECTION; i++) {
            try (Statement statement = statementList.get(i)) {
               // automatic resource cleanup
            } catch (SQLException e) {
               log.warn(//
                  "{} - Connection {} marked as broken because of an exception closing open statements during Connection.close()",//
                  poolEntry.getPoolName(),//
                  connection);//
               proxyLeakReportRunnable.cancel();
               poolEntry.evict("(exception closing Statements during Connection.close())");
               connection = ClosedConnection.CLOSED_CONNECTION;
            }
         }

         statementList.clear();
      }
   }
   //

   @Override
   public final void close() throws SQLException {
      // Closing statements can cause proxyConnection eviction, so this must run before the conditional below
      closeStatements();

      if (connection != ClosedConnection.CLOSED_CONNECTION) {
         proxyLeakReportRunnable.cancel();

         try {
            if (!isAutoCommit && commitStateIsDirty) {
               connection.rollback();
               lastAccess = currentTime();
               log.debug("{} - Executed rollback on proxyConnection {} due to dirty commit state on close().", poolEntry.getPoolName(), connection);
            }
            if (dirtyStatus != 0) {
               poolEntry.resetConnectionState(this, dirtyStatus);
               lastAccess = currentTime();
            }

            connection.clearWarnings();
         } catch (SQLException e) {
            // when connections are aborted, exceptions are often thrown that should not reach the application
            if (!poolEntry.isMarkedEvicted()) {
               throw checkException(e);
            }
         } finally {
            connection = ClosedConnection.CLOSED_CONNECTION;
            poolEntry.recycle(lastAccess);
         }
      }
   }


   @Override
   @SuppressWarnings("RedundantThrows")
   public boolean isClosed() throws SQLException {
      return (connection == ClosedConnection.CLOSED_CONNECTION);
   }
   //

   @Override
   public DatabaseMetaData getMetaData() throws SQLException {
      markCommitStateAsDirty();
      return connection.getMetaData();
   }

   //
   @Override
   public Statement createStatement() throws SQLException {
      return ProxyFactory.getProxyStatement(this, addStatement(connection.createStatement()));
   }

   @Override
   public Statement createStatement(int resultSetType, int concurrency) throws SQLException {
      return ProxyFactory.getProxyStatement(this, addStatement(connection.createStatement(resultSetType, concurrency)));
   }

   @Override
   public Statement createStatement(int resultSetType, int concurrency, int holdability) throws SQLException {
      return ProxyFactory.getProxyStatement(this, addStatement(connection.createStatement(resultSetType, concurrency, holdability)));
   }

   //
   //CallableStatement
   @Override
   public CallableStatement prepareCall(String sql) throws SQLException {
      throw new UnsupportedOperationException();
   }

   @Override
   public CallableStatement prepareCall(String sql, int resultSetType, int concurrency) throws SQLException {
      throw new UnsupportedOperationException();
   }

   @Override
   public CallableStatement prepareCall(String sql, int resultSetType, int concurrency, int holdability) throws SQLException {
      throw new UnsupportedOperationException();
   }

   //
   @Override
   public PreparedStatement prepareStatement(String sql) throws SQLException {
      return ProxyFactory.getProxyPreparedStatement(this, addStatement(connection.prepareStatement(sql)));
   }

   @Override
   public PreparedStatement prepareStatement(String sql, int resultSetType, int concurrency) throws SQLException {
      return ProxyFactory.getProxyPreparedStatement(this, addStatement(connection.prepareStatement(sql, resultSetType, concurrency)));
   }

   @Override
   public PreparedStatement prepareStatement(String sql, int resultSetType, int concurrency, int holdability) throws SQLException {
      return ProxyFactory.getProxyPreparedStatement(this, addStatement(connection.prepareStatement(sql, resultSetType, concurrency, holdability)));
   }

   //
   @Override
   public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException {
      return ProxyFactory.getProxyPreparedStatement(this, addStatement(connection.prepareStatement(sql, autoGeneratedKeys)));
   }

   @Override
   public PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException {
      throw new UnsupportedOperationException();
   }

   @Override
   public PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException {
      throw new UnsupportedOperationException();
   }

   //
   @Override
   public void setAutoCommit(boolean autoCommit) throws SQLException {
      connection.setAutoCommit(autoCommit);
      isAutoCommit = autoCommit;
      dirtyStatus |= dirty_status_autocommit;
   }

   @Override
   public void commit() throws SQLException {
      connection.commit();
      commitStateIsDirty = false;
      lastAccess = currentTime();
   }

   @Override
   public void rollback() throws SQLException {
      connection.rollback();
      commitStateIsDirty = false;
      lastAccess = currentTime();
   }

   @Override
   public void rollback(Savepoint savepoint) throws SQLException {
      connection.rollback(savepoint);
      commitStateIsDirty = false;
      lastAccess = currentTime();
   }
   //


   //
   @Override
   public void setReadOnly(boolean readOnly) throws SQLException {
      connection.setReadOnly(readOnly);
      isReadOnly = readOnly;
      commitStateIsDirty = false;
      dirtyStatus |= dirty_status_readonly;
   }


   @Override
   public void setTransactionIsolation(int level) throws SQLException {
      connection.setTransactionIsolation(level);
      transactionIsolation = level;
      dirtyStatus |= dirty_status_transaction_isolation_level;
   }


   @Override
   public void setCatalog(String catalog) throws SQLException {
      connection.setCatalog(catalog);
      this.catalog = catalog;
      dirtyStatus |= dirty_status_catalog;
   }


   @Override
   public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException {
      connection.setNetworkTimeout(executor, milliseconds);
      networkTimeout = milliseconds;
      dirtyStatus |= dirty_status_nettimeout;
   }


   @Override
   public void setSchema(String schema) throws SQLException {
      connection.setSchema(schema);
      this.schema = schema;
      dirtyStatus |= dirty_status_schema;
   }

   //
   @Override
   public final boolean isWrapperFor(Class<?> iface) throws SQLException {
      return iface.isInstance(connection) || (connection != null && connection.isWrapperFor(iface));
   }

   @Override
   @SuppressWarnings("unchecked")
   public final <T> T unwrap(Class<T> iface) throws SQLException {
      if (iface.isInstance(connection)) {
         return (T) connection;
      } else if (connection != null) {
         return connection.unwrap(iface);
      }

      throw new SQLException("Wrapped proxyConnection is not an instance of " + iface);
   }


}
